{
  Copyright 2012 Sergey Ostanin

  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at

      http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
}

unit QuestionList;

interface

uses
  Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, StdCtrls,
  ComCtrls, ActnList, Shape, superobject, LCLType, Menus, VisualUtils,
  Questions, Scene, CommCtrl, Clipbrd, TestUiUtils, InputString, ItParser,
  TestCore, base64, ItImport, ModifierListEditor, ItXmlizer, ItExport, MiscUtils;

type
  TQuestionListFrame = class(TShapeFrame)
    actCopy: TAction;
    actDelete: TAction;
    actCut: TAction;
    actDisable: TAction;
    actEnable: TAction;
    actDisableOrEnable: TAction;
    actEditModifiers: TAction;
    actMoveDown: TAction;
    actMoveUp: TAction;
    actSetWeight: TAction;
    alAddQuestion: TActionList;
    actSelectAll: TAction;
    actPaste: TAction;
    alQuestions: TActionList;
    ApplicationProperties: TApplicationProperties;
    ilQuestionList: TImageList;
    ilState: TImageList;
    lblQuestions: TLabel;
    lvQuestions: TListView;
    miEditModifiers: TMenuItem;
    miSeparator4: TMenuItem;
    miMoveDown: TMenuItem;
    miMoveUp: TMenuItem;
    miDisable: TMenuItem;
    miEnable: TMenuItem;
    miSeparator3: TMenuItem;
    miSetWeight: TMenuItem;
    miSeparator2: TMenuItem;
    miSeparator1: TMenuItem;
    miAdd: TMenuItem;
    miSelectAll: TMenuItem;
    miDelete: TMenuItem;
    miPaste: TMenuItem;
    miCopy: TMenuItem;
    miCut: TMenuItem;
    pmQuestionList: TPopupMenu;
    pmAddQuestion: TPopupMenu;
    tbrQuestions: TToolBar;
    tbAdd: TToolButton;
    tbDelete: TToolButton;
    tbSeparator1: TToolButton;
    tbSetWeight: TToolButton;
    tbDisableOrEnable: TToolButton;
    tbSeparator2: TToolButton;
    tbMoveUp: TToolButton;
    tbMoveDown: TToolButton;
    tbEditModifiers: TToolButton;
    procedure actCopyExecute(Sender: TObject);
    procedure actCopyUpdate(Sender: TObject);
    procedure actCutExecute(Sender: TObject);
    procedure actCutUpdate(Sender: TObject);
    procedure actDeleteExecute(Sender: TObject);
    procedure actDeleteUpdate(Sender: TObject);
    procedure actDisableExecute(Sender: TObject);
    procedure actDisableOrEnableExecute(Sender: TObject);
    procedure actEditModifiersExecute(Sender: TObject);
    procedure actEnableExecute(Sender: TObject);
    procedure actMoveDownExecute(Sender: TObject);
    procedure actMoveUpExecute(Sender: TObject);
    procedure actPasteExecute(Sender: TObject);
    procedure actPasteUpdate(Sender: TObject);
    procedure actSelectAllExecute(Sender: TObject);
    procedure actSetWeightExecute(Sender: TObject);
    procedure actSetWeightUpdate(Sender: TObject);
    procedure ApplicationPropertiesIdle(Sender: TObject; var Done: Boolean);
    procedure lvQuestionsAdvancedCustomDrawItem(Sender: TCustomListView;
      Item: TListItem; State: TCustomDrawState; Stage: TCustomDrawStage;
      var DefaultDraw: Boolean);
    procedure lvQuestionsContextPopup(Sender: TObject; MousePos: TPoint;
      var Handled: Boolean);
    procedure lvQuestionsResize(Sender: TObject);
    procedure lvQuestionsSelectItem(Sender: TObject; Item: TListItem;
      Selected: Boolean);
  private
    FRendered: Boolean;
    FSelectionChanged: Boolean;
    FItemEditors: TSuperObjectList;
    FSelection: ISuperObject;
    FQuestionAdded: Boolean;
    FMaxWeight: Integer;
    FPopupMenuRequested: Boolean;
    FModifierStateIndex: Integer;
    function ItemEditorIds: TSuperArray;
    function ItemEditorCount: Integer;
    procedure RenderItemEditors(ChangedOnly: Boolean);
    procedure RenderItemEditor(const Editor: ISuperObject; Index: Integer);
    procedure RenderSelection;
    procedure AutoSizeColumn;
    procedure AddQuestionExecute(Sender: TObject);
    procedure SelectNewQuestionKind(Index: Integer);
    function ValidWeightString(const s: String): Boolean;
    procedure UpdateActions;
    procedure EnableQuestions(Enable: Boolean);
    procedure MoveQuestions(Down: Boolean);
    function GetSelectedIndex: Integer;
    function GetSelectedItemEditor: ISuperObject;
    procedure GetModifiers(List: TModifierList);
    procedure SetModifiers(List: TModifierList);
    { private declarations }
  public
    procedure AfterConstruction; override;
    destructor Destroy; override;
    procedure Render;

    property QuestionAdded: Boolean read FQuestionAdded;
    { public declarations }
  end; 

implementation

{$R *.lfm}
{$R *.rc}

resourcestring
  SAdd = 'Add';
  SSetWeight = 'Set Weight';
  SQuestionWeight = 'Question weight:';
  SUseTitleCase = '1';

const
  NUMBER_COLUMN = 0;
  TEXT_COLUMN = 1;
  WEIGHT_COLUMN = 2;

{ TQuestionListFrame }

procedure TQuestionListFrame.lvQuestionsSelectItem(Sender: TObject;
  Item: TListItem; Selected: Boolean);
begin
  FSelectionChanged := TRUE;
end;

procedure TQuestionListFrame.actCopyExecute(Sender: TObject);
begin
  Dispatcher.PerformAction(Id, 'copy');
  SetClipboardMarkerFormat(QUESTION_LIST_CLIPBOARD_FORMAT);
end;

procedure TQuestionListFrame.actCopyUpdate(Sender: TObject);
begin
  (Sender as TAction).Enabled := ListViewHasSelection(lvQuestions);
end;

procedure TQuestionListFrame.actCutExecute(Sender: TObject);
begin
  Dispatcher.PerformAction(Id, 'cut');
  SetClipboardMarkerFormat(QUESTION_LIST_CLIPBOARD_FORMAT);
end;

procedure TQuestionListFrame.actCutUpdate(Sender: TObject);
begin
  (Sender as TAction).Enabled := ListViewHasSelection(lvQuestions);
end;

procedure TQuestionListFrame.actDeleteExecute(Sender: TObject);
begin
  Dispatcher.PerformAction(Id, 'delete');
end;

procedure TQuestionListFrame.actDeleteUpdate(Sender: TObject);
begin
  (Sender as TAction).Enabled := ListViewHasSelection(lvQuestions);
end;

procedure TQuestionListFrame.actDisableExecute(Sender: TObject);
begin
  EnableQuestions(FALSE);
end;

procedure TQuestionListFrame.actDisableOrEnableExecute(Sender: TObject);
begin
  EnableQuestions(actDisableOrEnable.Checked);
end;

procedure TQuestionListFrame.actEditModifiersExecute(Sender: TObject);
var
  ModifierList: TModifierList;
begin
  ModifierList := TModifierList.Create;
  try
    GetModifiers(ModifierList);

    if TModifierListEditorForm.OpenModal(ModifierList,
      StringToKind(GetSelectedItemEditor.S['questionKind'])) then
      SetModifiers(ModifierList);
  finally
    ModifierList.Free;
  end;
end;

procedure TQuestionListFrame.actEnableExecute(Sender: TObject);
begin
  EnableQuestions(TRUE);
end;

procedure TQuestionListFrame.actMoveDownExecute(Sender: TObject);
begin
  MoveQuestions(TRUE);
end;

procedure TQuestionListFrame.actMoveUpExecute(Sender: TObject);
begin
  MoveQuestions(FALSE);
end;

procedure TQuestionListFrame.actPasteExecute(Sender: TObject);
begin
  Dispatcher.PerformAction(Id, 'paste');
end;

procedure TQuestionListFrame.actPasteUpdate(Sender: TObject);
begin
  (Sender as TAction).Enabled := ClipboardHasFormatName(QUESTION_LIST_CLIPBOARD_FORMAT);
end;

procedure TQuestionListFrame.actSelectAllExecute(Sender: TObject);
begin
  SelectAllListViewItems(lvQuestions);
end;

procedure TQuestionListFrame.actSetWeightExecute(Sender: TObject);
var
  w, i: Integer;
  s: String;
  Input: ISuperObject;
begin
  w := 0;
  for i := 0 to FItemEditors.Count-1 do
  begin
    if lvQuestions.Items[i].Selected and (FItemEditors[i].I['weight'] <> w) then
    begin
      if w = 0 then
        w := FItemEditors[i].I['weight']
      else
      begin
        w := 0;
        Break;
      end;
    end;
  end;
  if w = 0 then
    s := ''
  else
    s := IntToStr(w);

  if TInputStringForm.OpenModal(SSetWeight, SQuestionWeight, s,
    Length(IntToStr(FMaxWeight)), ValidWeightString) then
  begin
    Input := SO;
    Input.I['weight'] := StrToInt(Trim(s));
    Dispatcher.PerformAction(Id, 'setWeight', Input);
  end;
end;

procedure TQuestionListFrame.actSetWeightUpdate(Sender: TObject);
begin
  (Sender as TAction).Enabled := ListViewHasSelection(lvQuestions);
end;

procedure TQuestionListFrame.ApplicationPropertiesIdle(Sender: TObject;
  var Done: Boolean);
var
  Indices, Input: ISuperObject;
  i: Integer;
begin
  if FSelectionChanged then
  begin
    FSelectionChanged := FALSE;

    Indices := SA([]);
    for i := 0 to lvQuestions.Items.Count-1 do
      if lvQuestions.Items[i].Selected then
        Indices.AsArray.Add(SO(i));

    Input := SO;
    Input['indices'] := Indices;
    Dispatcher.PerformAction(Id, 'select', Input);
  end
  else if FPopupMenuRequested then
  begin
    FPopupMenuRequested := FALSE;
    pmQuestionList.PopUp;
  end;
end;

procedure TQuestionListFrame.lvQuestionsAdvancedCustomDrawItem(
  Sender: TCustomListView; Item: TListItem; State: TCustomDrawState;
  Stage: TCustomDrawStage; var DefaultDraw: Boolean);
var
  r: TRect;
  y: Integer;
begin
  if not FItemEditors[Item.Index].B['enabled'] then
  begin
    if Stage = cdPrePaint then
      lvQuestions.Canvas.Font.Color := clGrayText
    else if Stage = cdPostPaint then
    begin
      r := Item.DisplayRect(drBounds);
      y := (r.Top + r.Bottom) div 2 + 1;
      lvQuestions.Canvas.Pen.Color := clGrayText;
      lvQuestions.Canvas.Line(r.Left, y, r.Right, y);
    end;
  end;
end;

procedure TQuestionListFrame.lvQuestionsContextPopup(Sender: TObject;
  MousePos: TPoint; var Handled: Boolean);
begin
  FPopupMenuRequested := TRUE;
  Handled := TRUE;
end;

procedure TQuestionListFrame.lvQuestionsResize(Sender: TObject);
begin
  AutoSizeColumn;
end;

function TQuestionListFrame.ItemEditorIds: TSuperArray;
begin
  Result := Shape.A['itemEditor'];
end;

function TQuestionListFrame.ItemEditorCount: Integer;
begin
  Result := ItemEditorIds.Length;
end;

procedure TQuestionListFrame.RenderItemEditors(ChangedOnly: Boolean);
var
  i: Integer;
  Ids: TSuperArray;
  Editor: ISuperObject;
  EditorChanged: Boolean;
begin
  Ids := ItemEditorIds;
  for i := 0 to FItemEditors.Count-1 do
  begin
    Editor := FItemEditors[i];
    EditorChanged := Scene.Update(Ids.S[i], Editor);
    if EditorChanged then
      FItemEditors[i] := Editor;

    if not ChangedOnly or EditorChanged then
      RenderItemEditor(Editor, i);
  end;
end;

procedure TQuestionListFrame.RenderItemEditor(const Editor: ISuperObject;
  Index: Integer);
var
  Item: TListItem;
begin
  Item := lvQuestions.Items[Index];
  Item.ImageIndex := QuestionRegistry.Find(StringToKind(Editor.AsObject.S['questionKind']));
  Item.Caption := IntToStr(Index+1);
  Item.SubItems[TEXT_COLUMN-1] := Editor.AsObject.S['text'];
  Item.SubItems[WEIGHT_COLUMN-1] := IntToStr(Editor.AsObject.I['weight']);

  if Editor.AsObject.B['hasModifiers'] then
    Item.StateIndex := FModifierStateIndex
  else
    Item.StateIndex := -1;
end;

procedure TQuestionListFrame.RenderSelection;
var
  i, Index: Integer;
  SelectedIndex: ISuperObject;
  Sel: array of Boolean;
begin
  SetLength(Sel, ItemEditorCount);
  for i := 0 to High(Sel) do
    Sel[i] := FALSE;

  for SelectedIndex in FSelection['selectedIndex'] do
  begin
    Index := SelectedIndex.AsInteger;
    Assert( Index >= 0 );
    Assert( Index < Length(Sel) );
    Sel[Index] := TRUE;
  end;

  for i := 0 to High(Sel) do
    lvQuestions.Items[i].Selected := Sel[i];
end;

procedure TQuestionListFrame.AutoSizeColumn;
begin
  AutoSizeListViewColumn(lvQuestions, TEXT_COLUMN);
end;

procedure TQuestionListFrame.AddQuestionExecute(Sender: TObject);
var
  KindIndex: Integer;
  Input: ISuperObject;
begin
  KindIndex := (Sender as TAction).Tag;
  SelectNewQuestionKind(KindIndex);

  Input := SO;
  Input.S['kind'] := KindToString(QuestionRegistry.GetKind(KindIndex));
  Input.S['initialText'] := QuestionRegistry.GetInitialText(KindIndex);
  Dispatcher.PerformAction(Id, 'add', Input);

  FQuestionAdded := TRUE;
end;

procedure TQuestionListFrame.SelectNewQuestionKind(Index: Integer);
var
  SelectedAction: TAction;
  Action: TContainedAction;
  Item: TMenuItem;
begin
  SelectedAction := alAddQuestion[Index] as TAction;

  for Action in alAddQuestion do
    (Action as TAction).ShortCut := 0;
  SelectedAction.ShortCut := VK_F2;

  for Item in pmAddQuestion.Items do
    Item.Default := FALSE;
  pmAddQuestion.Items[Index].Default := TRUE;

  for Item in miAdd do
    Item.Default := FALSE;
  miAdd[Index].Default := TRUE;

  tbAdd.Action := SelectedAction;
end;

function TQuestionListFrame.ValidWeightString(const s: String): Boolean;
var
  w: Integer;
begin
  Result := TryStrToInt(Trim(s), w) and (w >= 1) and (w <= FMaxWeight);
end;

procedure TQuestionListFrame.UpdateActions;
var
  CanDisable, CanEnable, HasSelection: Boolean;
  i: Integer;
begin
  CanDisable := FALSE;
  CanEnable := FALSE;
  HasSelection := FALSE;

  for i := 0 to FItemEditors.Count-1 do
  begin
    if lvQuestions.Items[i].Selected then
    begin
      HasSelection := TRUE;
      if FItemEditors[i].B['enabled'] then
        CanDisable := TRUE
      else
        CanEnable := TRUE;
      if CanDisable and CanEnable then
        Break;
    end;
  end;

  actDisable.Enabled := CanDisable;
  actEnable.Enabled := CanEnable;
  actDisableOrEnable.Enabled := HasSelection;
  actDisableOrEnable.Checked := CanEnable;

  actMoveUp.Enabled := HasSelection and not lvQuestions.Items[0].Selected;
  actMoveDown.Enabled := HasSelection
    and not lvQuestions.Items[lvQuestions.Items.Count-1].Selected;

  actEditModifiers.Enabled := FSelection.A['selectedIndex'].Length = 1;
end;

procedure TQuestionListFrame.EnableQuestions(Enable: Boolean);
var
  Input: ISuperObject;
begin
  Input := SO;
  Input.B['enable'] := Enable;
  Dispatcher.PerformAction(Id, 'enable', Input);
end;

procedure TQuestionListFrame.MoveQuestions(Down: Boolean);
var
  Input: ISuperObject;
begin
  Input := SO;
  Input.B['forward'] := Down;
  Dispatcher.PerformAction(Id, 'move', Input);
end;

function TQuestionListFrame.GetSelectedIndex: Integer;
begin
  Result := FSelection.A['selectedIndex'].I[0];
end;

function TQuestionListFrame.GetSelectedItemEditor: ISuperObject;
begin
  Result := FItemEditors[GetSelectedIndex];
end;

procedure TQuestionListFrame.GetModifiers(List: TModifierList);
var
  Res: ISuperObject;
begin
  Res := Dispatcher.PerformAction(ItemEditorIds.S[GetSelectedIndex], 'getModifiers');
  BytesToModifierList(DecodeStringBase64(Res.S['modifiers']), List);
end;

procedure TQuestionListFrame.SetModifiers(List: TModifierList);
var
  Input: ISuperObject;
begin
  Input := SO;
  Input.S['modifiers'] := EncodeStringBase64(ModifierListToBytes(List));
  Dispatcher.PerformAction(ItemEditorIds.S[GetSelectedIndex], 'setModifiers', Input);
end;

procedure TQuestionListFrame.AfterConstruction;
var
  i: Integer;
  Action: TAction;
  Title, UiTitle: String;
begin
  inherited;
  FMaxWeight := Shape.I['maxWeight'];

  for i := 0 to QuestionRegistry.Count-1 do
  begin
    AddPngResourceToImageList(QuestionRegistry.GetIconResourceName(i), ilQuestionList);

    Action := TAction.Create(alAddQuestion);
    Action.ActionList := alAddQuestion;
    Action.Tag := i;
    Action.ImageIndex := i;
    Title := QuestionRegistry.GetTitle(i);
    Action.Caption := Title;
    if StringToBoolean(SUseTitleCase) then
      UiTitle := Title
    else
      UiTitle := ToLowerUtf8(Title);
    Action.Hint := Format('%s %s', [SAdd, UiTitle]);
    Action.OnExecute := AddQuestionExecute;

    AddMenuItemForAction(Action, pmAddQuestion.Items);
    AddMenuItemForAction(Action, miAdd);
  end;

  if QuestionRegistry.Count > 0 then
    SelectNewQuestionKind(0);

  alQuestions.OnExecute := Scene.ActionListExecuteFilter;
  alAddQuestion.OnExecute := Scene.ActionListExecuteFilter;
  LoadActionImages(alQuestions);

  MakeListViewDoubleBuffered(lvQuestions);

  lvQuestions.Columns[NUMBER_COLUMN].Width := ilState.Width + ilQuestionList.Width
    + lvQuestions.Canvas.TextWidth(StringOfChar('0', 6));
  lvQuestions.Columns[WEIGHT_COLUMN].Width := lvQuestions.Canvas.TextWidth(
    StringOfChar('0', 9));
  AutoSizeColumn;

  FItemEditors := TSuperObjectList.Create;
  FModifierStateIndex := AddPngResourceToImageList('QuestionList/modifier.png', ilState);
end;

destructor TQuestionListFrame.Destroy;
begin
  FreeAndNil(FItemEditors);
  inherited;
end;

procedure TQuestionListFrame.Render;
var
  Item: TListItem;
  ShapeChanged, SelectionChanged: Boolean;
begin
  ShapeChanged := UpdateShape;
  SelectionChanged := Scene.Update(Shape.S['selection'], FSelection);

  if not FRendered or ShapeChanged then
  begin
    FItemEditors.Clear;
    FItemEditors.Count := ItemEditorCount;
    SetListViewItemCount(lvQuestions, ItemEditorCount, 2);

    RenderItemEditors(FALSE);
    RenderSelection;

    Item := GetListViewLastSelectedItem(lvQuestions);
    if Item <> nil then
    begin
      lvQuestions.ItemFocused := Item;
      ListView_SetSelectionMark(lvQuestions.Handle, Item.Index);
      Item.MakeVisible(FALSE);
    end;
  end
  else
  begin
    RenderItemEditors(TRUE);
    if SelectionChanged then
      RenderSelection;
  end;

  UpdateActions;

  FRendered := TRUE;
  FQuestionAdded := FALSE;
  FSelectionChanged := FALSE;
end;

end.

